package com.ideaaedi.zoo.example.springboot.flowable.sharding.controller.helper.service;

import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.map.MapUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONWriter;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.ideaaedi.commonds.time.DateTimeConverter;
import com.ideaaedi.zoo.diy.artifact.flowable.convert.BpmnEntityConvert;
import com.ideaaedi.zoo.diy.artifact.flowable.entity.form.BpmnFormCreate;
import com.ideaaedi.zoo.diy.artifact.flowable.facade.*;
import com.ideaaedi.zoo.diy.artifact.flowable.facade.form.BpmnFormData;
import com.ideaaedi.zoo.diy.artifact.flowable.facade.form.BpmnFormProperty;
import com.ideaaedi.zoo.diy.artifact.flowable.facade.form.DefaultBpmnFormProperty;
import com.ideaaedi.zoo.diy.artifact.flowable.service.form.BpmnFormRepositoryService;
import com.ideaaedi.zoo.diy.artifact.flowable.service.form.BpmnFormService;
import com.ideaaedi.zoo.example.springboot.flowable.sharding.controller.helper.entity.PageInfo;
import com.ideaaedi.zoo.example.springboot.flowable.sharding.controller.helper.entity.enums.FlowableProcessFlag;
import com.ideaaedi.zoo.example.springboot.flowable.sharding.controller.helper.entity.enums.LeaveApprovalState;
import com.ideaaedi.zoo.example.springboot.flowable.sharding.controller.helper.entity.enums.LeaveType;
import com.ideaaedi.zoo.example.springboot.flowable.sharding.controller.helper.entity.po.DemoLeaveInfoPO;
import com.ideaaedi.zoo.example.springboot.flowable.sharding.controller.helper.entity.req.*;
import com.ideaaedi.zoo.example.springboot.flowable.sharding.controller.helper.entity.resp.DemoLeaveInfoDetailRespVO;
import com.ideaaedi.zoo.example.springboot.flowable.sharding.controller.helper.entity.resp.DemoLeaveInfoListRespVO;
import com.ideaaedi.zoo.example.springboot.flowable.sharding.controller.helper.mapper.DemoLeaveInfoMapper;
import com.ideaaedi.zoo.example.springboot.flowable.sharding.entity.dto.*;
import com.ideaaedi.zoo.example.springboot.flowable.sharding.entity.tuple.StrObjVar;
import com.ideaaedi.zoo.example.springboot.flowable.sharding.event.SyncFlowableProcessHisEvent;
import com.ideaaedi.zoo.example.springboot.flowable.sharding.service.ShardingBpmnHistoryService;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Triple;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.Process;
import org.flowable.bpmn.model.UserTask;
import org.flowable.common.engine.impl.identity.Authentication;
import org.flowable.engine.HistoryService;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.TaskQuery;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * <p>
 * 请假申请 服务实现类
 * </p>
 *
 * @author Administrator
 * @since 1.0.0
 */
@Slf4j
@Service
public class DemoLeaveInfoServiceImpl extends ServiceImpl<DemoLeaveInfoMapper, DemoLeaveInfoPO> implements DemoLeaveInfoService, ApplicationRunner {
    
    /**
     * 假设当前操作都在此租户下
     */
    private static final String TENANT = "1,3,";
    
    /**
     * 启动流程时，对流程变量进行约束的表单
     */
    private static final String START_FORM_KEY = "demo4StartProcessFormKey";
    
    /**
     * 初审关联的表单标识
     */
    private static final String AUDIT_FORM_KEY = "demo4AuditFormKey";
    
    /**
     * 复审关联的表单标识
     */
    private static final String REVIEW_FORM_KEY = "demo4ReviewFormKey";
    
    @Resource
    private BpmnFormRepositoryService bpmnFormRepositoryService;
    @Resource
    private HistoryService historyService;

    @Resource
    private RepositoryService repositoryService;

    @Resource
    private RuntimeService runtimeService;

    @Resource
    private TaskService taskService;
    
    @Resource
    private ShardingBpmnHistoryService shardingBpmnHistoryService;
    
    @Resource
    private BpmnFormService bpmnFormService;
    
    @Resource
    private ApplicationContext applicationContext;
    
    /**
     * 假设 我们要使用的流程定义是这个
     */
    private String processDefinitionKey;
    private String processDefinitionId;
    
    /**
     * 任务本地变量记录任务的完成人， key
     */
    private static final String LOCAL_TASK_COMPLETED_BY = "__LOCAL_TASK_COMPLETED_BY__";
    
    /**
     * 任务本地变量记录任务的表单值， key
     */
    private static final String LOCAL_TASK_FORM = "__LOCAL_TASK_FORM__";
    
    private final Long  userId = 9527L; // 假设流程发起人是9527L
    private final Long approvalId = 1994L; // 假设流程发起人的上级领导是1994L
    private final Long reviewUserId = 888L; // 假设复审人是888L
    /*
     * 假设他们有这些 流程角色组（可灵活对应业务中的角色、部门之类的权限组）
     */
    private final Map<Long, Collection<String>> userIdAndGroupMap = Map.of(
            userId, Sets.newHashSet(),
            approvalId, Sets.newHashSet(),
            /*
             * 假设复审人所在的流程角色组有：
             *    ROLE_MANAGER：对应业务系统中，这个人拥有角色MANAGER
             *    DEPT_JITUAN：对应业务系统中，这个人所在部门JITUAN
             *
             * 注：名称自定义即可，只要流程定义中写的和这里能对应上就行
             */
            reviewUserId, Sets.newHashSet("ROLE_MANAGER", "DEPT_JITUAN")
    );
    
    /**
     * 使用流程前的准备工作
     */
    @Override
    public void run(ApplicationArguments args) {
        // step1. 设计工作流，并获取到流程设计文件 xxx.bpmn20.xml
        String processDesignFileResource = "工作流与业务结合-请假申请.bpmn20.xml";
        
        // step2. 部署工作流设计文件 xxx.bpmn20.xml（即：新增流程定义） , 并获取到部署的流程定义的唯一key
        // 部署工作流设计文件 xxx.bpmn20.xml（即：新增流程定义）
        Deployment deployment = repositoryService.createDeployment().tenantId(TENANT)
                .addBytes("工作流与业务结合-请假申请.bpmn20.xml",
                        ResourceUtil.readBytes(processDesignFileResource))
                .name("工作流与业务结合-请假申请")
                .category("内部行政管理")
                .deploy();
        BpmnDeployment bpmnDeployment = BpmnEntityConvert.convert2BpmnDeployment(deployment);

        // 获取到部署的流程定义的唯一key
        List<? extends BpmnProcessDefinition> bpmnProcessDefinitions = Optional.ofNullable(
                repositoryService.createProcessDefinitionQuery()
                        .processDefinitionTenantIdLike(TENANT + "%")
                        .deploymentId(bpmnDeployment.getId())
                        .list()
        ).map(x -> x.stream()
                .map(BpmnEntityConvert::convert2BpmnProcessDefinition)
                .toList()
        ).orElse(Collections.emptyList());

        if (bpmnProcessDefinitions.size() != 1) {
            throw new IllegalStateException("未查询到流程定义");
        }
        processDefinitionKey = bpmnProcessDefinitions.get(0).getKey();
        processDefinitionId = bpmnProcessDefinitions.get(0).getId();
    
        // step3. 确保数据库中存在工作流中需要用到的表单（在设计工作流时会指定需要用到的表单的标识，如果需要用到表单的话）
        bpmnFormRepositoryService.create(TENANT,
                BpmnFormCreate.builder()
                        .formKey(START_FORM_KEY)
                        .formName("请假流程 - 启动流程时的至少需要的流程变量约束")
                        .formProperties(
                                Lists.newArrayList(
                                        DefaultBpmnFormProperty.builder()
                                                .key("sponsor")
                                                .name("发起人")
                                                .type(String.class.getName())
                                                .ifRequired(true)
                                                .build(),
                                        DefaultBpmnFormProperty.builder()
                                                .key("approver")
                                                .name("初审人")
                                                .type(String.class.getName())
                                                .ifRequired(true)
                                                .build(),
                                        DefaultBpmnFormProperty.builder()
                                                .key("approvalState")
                                                .name("流程状态")
                                                .type(String.class.getName())
                                                .ifRequired(true)
                                                .build()
                                )
                        ).build()
        );
        bpmnFormRepositoryService.create(TENANT,
                BpmnFormCreate.builder()
                        .formKey(AUDIT_FORM_KEY)
                        .formName("请假流程 - 初审表单")
                        .formProperties(
                                Lists.newArrayList(
                                        DefaultBpmnFormProperty.builder()
                                                .key("auditRemark")
                                                .name("初审意见")
                                                .type(String.class.getName())
                                                .description("初审意见")
                                                .ifRequired(true)
                                                .build()
                                )
                        ).build()
        );
        bpmnFormRepositoryService.create(TENANT,
                BpmnFormCreate.builder()
                        .formKey(REVIEW_FORM_KEY)
                        .formName("请假流程 - 复审表单")
                        .formProperties(
                                Lists.newArrayList(
                                        DefaultBpmnFormProperty.builder()
                                                .key("reviewRemark")
                                                .name("复审意见")
                                                .type(String.class.getName())
                                                .description("复审意见")
                                                .ifRequired(true)
                                                .build()
                                )
                        ).build()
        );
    }

    @Nullable
    @Override
    @DSTransactional(rollbackFor = Exception.class)
    public DemoLeaveInfoDetailRespVO add(DemoLeaveInfoAddReqVO req, boolean queryDetail) {
        DemoLeaveInfoPO po = new DemoLeaveInfoPO();
        BeanUtils.copyProperties(req, po);
        po.setId(null);
        if (BooleanUtils.isTrue(req.getAsDraft())) {
            po.setApprovalState(LeaveApprovalState.DRAFT);
            save(po);
        } else {
            po.setApprovalState(LeaveApprovalState.TO_AUDIT);
            save(po);
            // 发起流程
            releaseProcess(po);
        }
        return queryDetail ? applyDetail(po.getId()) : null;
    }
    
    @Override
    @DSTransactional(rollbackFor = Exception.class)
    public DemoLeaveInfoDetailRespVO release(Long id, boolean testTransaction) {
        DemoLeaveInfoPO po = getById(id);
        if (po.getApprovalState() != LeaveApprovalState.DRAFT) {
            throw new IllegalStateException("只有草稿态的申请才能发起");
        }
        // 修改业务表中的对应状态
        boolean success = update(
                Wrappers.lambdaUpdate(DemoLeaveInfoPO.class)
                        .set(DemoLeaveInfoPO::getApprovalState, LeaveApprovalState.TO_AUDIT)
                        .eq(DemoLeaveInfoPO::getApprovalState, LeaveApprovalState.DRAFT)
                        .eq(DemoLeaveInfoPO::getId, id)
        );
        if (!success) {
            throw new IllegalStateException("数据状态发生了变化，请刷新后重试");
        }
        po.setApprovalState(LeaveApprovalState.TO_AUDIT);
        
        // 发起流程
        releaseProcess(po);
    
        if (testTransaction) {
            throw new RuntimeException("抛出异常，触发事务回滚");
        }
        return applyDetail(id);
    }
    
    @Override
    @DSTransactional(rollbackFor = Exception.class)
    public DemoLeaveInfoDetailRespVO delete(Long id) {
        Objects.requireNonNull(id, "id cannot be null.");
        DemoLeaveInfoDetailRespVO resp = applyDetail(id);
        // 删除业务数据
        removeById(id);
        // 删除对应的流程实例 （注：此方法删除的数据，在流程历史信息中能找到）
        BpmnProcessInstance bpmnProcessInstance = Optional.ofNullable(
                        runtimeService.createProcessInstanceQuery().processInstanceTenantIdLike(TENANT + "%")
                                .processInstanceBusinessKey(id + "").singleResult()
                ).map(BpmnEntityConvert::convert2BpmnProcessInstance)
                .orElse(null);

        if (bpmnProcessInstance != null) {
            runtimeService.deleteProcessInstance(bpmnProcessInstance.getId(), null);
            // 发布历史数据同步事件
            applicationContext.publishEvent(new SyncFlowableProcessHisEvent(bpmnProcessInstance.getId()));
        }
        return resp;
    }
    
    @Override
    public DemoLeaveInfoDetailRespVO terminate(Long id) {
        Objects.requireNonNull(id, "id cannot be null.");
        // 删除（或修改）业务数据
        removeById(id);
        //updateToTerminate(id);
        
        // 终止对应的流程实例 （实际也是调用删除流程实例的接口. 注：此方法删除的数据，在流程历史信息中能找到）
        BpmnProcessInstance bpmnProcessInstance = Optional.ofNullable(
                        runtimeService.createProcessInstanceQuery().processInstanceTenantIdLike(TENANT + "%")
                                .processInstanceBusinessKey(id + "").singleResult()
                ).map(BpmnEntityConvert::convert2BpmnProcessInstance)
                .orElse(null);
        if (bpmnProcessInstance != null) {
            runtimeService.deleteProcessInstance(bpmnProcessInstance.getId(), "终止");
            // 发布历史数据同步事件
            applicationContext.publishEvent(new SyncFlowableProcessHisEvent(bpmnProcessInstance.getId()));
        }
        return applyDetail(id);
    }
    
    @Nullable
    @Override
    @DSTransactional(rollbackFor = Exception.class)
    public DemoLeaveInfoDetailRespVO update(DemoLeaveInfoUpdateReqVO req) {
        Long id = req.getId();
        Objects.requireNonNull(id, "id cannot be null.");
        DemoLeaveInfoPO po = new DemoLeaveInfoPO();
        BeanUtils.copyProperties(req, po);
        updateById(po);
    

        boolean needUpdateProcessVar = req.getApprovalState() != null
                && req.getApprovalState() != LeaveApprovalState.DRAFT; // todo 需要根据自己的情况，判断是否需要修改流程实例中的流程变量
        if (needUpdateProcessVar) {
            Set<LeaveApprovalState> nowAllowUpdated = Sets.newHashSet(
                    LeaveApprovalState.DRAFT,
                    LeaveApprovalState.AUDIT_REJECT,
                    LeaveApprovalState.REVIEW_REJECT,
                    LeaveApprovalState.REVIEW_PASS
            );
            if (nowAllowUpdated.contains(req.getApprovalState())) {
                throw new IllegalStateException("涉及到工作流审批，不允许直接修改，请重新发起流程");
            }
            /// step1. ------------------------- 假设update了业务表中的approvalState字段，而该字段也有一份在流程实例的流程变量中
            /// step1. ------------------------- 那么我们也需要修改流程变量中的该值
            // 获取对应的流程实例
            BpmnProcessInstance bpmnProcessInstance = Optional.ofNullable(
                            runtimeService.createProcessInstanceQuery().processInstanceTenantIdLike(TENANT + "%")
                                    .processInstanceBusinessKey(id + "").singleResult()
                    ).map(BpmnEntityConvert::convert2BpmnProcessInstance)
                    .orElse(null);
            Objects.requireNonNull(bpmnProcessInstance, "bpmnProcessInstance should not be null.");
            List<? extends BpmnTask> list = Optional.ofNullable(
                    taskService.createTaskQuery()
                            .taskTenantIdLike(TENANT + "%")
                            .processInstanceId(bpmnProcessInstance.getId())
                            .list()
            ).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnTask)
                    .toList()
            ).orElse(Collections.emptyList());

            // 随便用一个taskId进行流程变量的修改即可 （注：一个流程的流程变量是全局的，仅有一份，你在【此时此地】修改，那么从【他时他地】查看时，也跟着变化了）
            if (list.size() > 0) {
                BpmnTask bpmnTask = list.get(0);
                runtimeService.setVariable(bpmnTask.getProcessInstanceId(), "approvalState", req.getApprovalState());
            }
            
            /// step2. 信息修改后，流程节点是否需要修改？
            boolean needChangeProcessNode = false; // todo 根据业务情况，判断是否需要修改流程节点
            //noinspection ConstantValue
            if (needChangeProcessNode) {
                throw new IllegalStateException("不允许修改，请重新发起流程");
            }
            
            // 发布历史数据同步事件
            applicationContext.publishEvent(new SyncFlowableProcessHisEvent(bpmnProcessInstance.getId()));
        }
        return applyDetail(id);
    }
    
    @Nullable
    @Override
    @DSTransactional(rollbackFor = Exception.class)
    public DemoLeaveInfoDetailRespVO complete(DemoLeaveInfoCompleteReqVO req) {
        String taskId = req.getTaskId();
        LeaveApprovalState approvalState = req.getApprovalState();
        Long userId = req.getUserId();
        Map<String, Object> formData = req.getFormData();
        List<String> commentList = req.getCommentList();
        List<DemoLeaveInfoCompleteReqVO.AttachmentDTO> attachmentList = req.getAttachmentList();
    
        // ---------------------------------------------------------- 从工作流中查询相关流程数据
        BpmnTask task = Optional.ofNullable(
                taskService.createTaskQuery()
                        .taskTenantIdLike(TENANT + "%")
                        .taskId(taskId)
                        .includeTaskLocalVariables()
                        .includeProcessVariables()
                        .singleResult()
            ).map(BpmnEntityConvert::convert2BpmnTask).orElse(null);
        Objects.requireNonNull(task, "task should not be null.");
        BpmnProcessInstance bpmnProcessInstance = Optional.ofNullable(
                        runtimeService.createProcessInstanceQuery().processInstanceTenantIdLike(TENANT + "%")
                                .processInstanceId(task.getProcessInstanceId()).singleResult()
                ).map(BpmnEntityConvert::convert2BpmnProcessInstance)
                .orElse(null);

        Objects.requireNonNull(bpmnProcessInstance, "processInstance should not be null.");
        String processInstanceId = bpmnProcessInstance.getId();
    
        // 校验参数 当前工作流流程变量中的审批状态
        LeaveApprovalState currApprovalState = Optional.ofNullable(task.getProcessVariables())
                .map(map -> map.get("approvalState"))
                .map(x -> LeaveApprovalState.valueOf(x.toString()))
                .orElse(null);
        Objects.requireNonNull(currApprovalState, "currApprovalState should not be null.");
    
        Set<LeaveApprovalState> nextNodes = currApprovalState.nextNodes();
        if (nextNodes == null || !nextNodes.contains(approvalState)) {
            throw new IllegalStateException("状态流转不合法， currApprovalState -> "
                    + currApprovalState + ", targetApprovalState -> " + approvalState);
        }
        
    
        // ---------------------------------------------------------- 更新业务数据表相关数据
        // 校验： 当前工作流流程变量中的审批状态 应该和当前数据库中的审批状态一致
        Long poId = Long.parseLong(bpmnProcessInstance.getBusinessKey());
        LeaveApprovalState currPoApprovalState = getOneOpt(
                Wrappers.lambdaQuery(DemoLeaveInfoPO.class)
                        .select(DemoLeaveInfoPO::getApprovalState)
                        .eq(DemoLeaveInfoPO::getId, poId)
        ).map(DemoLeaveInfoPO::getApprovalState).orElse(null);
        Assert.isTrue(currPoApprovalState == currApprovalState, "工作流的审批状态和业务数据表的审批状态不一致。 请联系管理员清洗脏数据，或作废数据重启发起审批");
    
        // 使用乐观锁更新数据库，因为可能存在并发修改
        boolean success = update(
                Wrappers.lambdaUpdate(DemoLeaveInfoPO.class)
                        .set(DemoLeaveInfoPO::getApprovalState, approvalState)
                        .eq(DemoLeaveInfoPO::getApprovalState, currApprovalState)
                        .eq(DemoLeaveInfoPO::getId, poId)
        );
        if (!success) {
            throw new IllegalStateException("数据状态发生了变化，请刷新后重试");
        }
        
        
        // ---------------------------------------------------------- 更新工作流相关数据
        
        // 评论
        if (!CollectionUtils.isEmpty(commentList)) {
            for (String comment : commentList) {
                taskService.addComment(taskId, processInstanceId, comment);
            }
        }
        
        // 附件
        if (!CollectionUtils.isEmpty(attachmentList)) {
            for (DemoLeaveInfoCompleteReqVO.AttachmentDTO attachment : attachmentList) {
                taskService.createAttachment(
                        attachment.getType(),
                        taskId,
                        processInstanceId,
                        attachment.getName(),
                        attachment.getDescription(),
                        // 这里url字段存数据库的id吧，查询时转换成url即可
                        attachment.getFileId() + "");
            }
        }
        
        // 表单（作为局部变量记录进任务中）
        // 校验表单（如果有的话）中要求的必填参数是否填了
        BpmnFormData taskFormData = bpmnFormService.getTaskFormData(TENANT, taskId);
        List<? extends BpmnFormProperty> bpmnFormProperties =
                Optional.ofNullable(taskFormData).map(BpmnFormData::getFormProperties).orElse(Collections.emptyList());
        if (!CollectionUtils.isEmpty(bpmnFormProperties)) {
            Set<String> requiredKeySet = bpmnFormProperties.stream().filter(x -> BooleanUtils.isTrue(x.getIfRequired()))
                    .map(BpmnFormProperty::getKey).collect(Collectors.toSet());
            if (!CollectionUtils.isEmpty(requiredKeySet)) { // 必填项
                if (formData == null) {
                    throw new IllegalStateException("表单数据不能为空");
                }
                for (String requiredKey : requiredKeySet) {
                    Object value = formData.get(requiredKey);
                    Objects.requireNonNull(value, "表单数据中，key为" + requiredKey + "的值必填");
                }
            }
        }
        // 记录表单
        taskService.setVariableLocal(taskId, LOCAL_TASK_FORM, formData);
        
        // 用局部变量记录任务完成人
        taskService.setVariableLocal(taskId, LOCAL_TASK_COMPLETED_BY, userId);
        // 同时这里也传完成人，这里的完成人会记录进历史任务的completedBy字段，但是我们查询时以局部变量中的完成人为条件进行查询
        taskService.complete(taskId, userId + "", Map.of("approvalState", approvalState.name()));
    
    

        // 发布历史数据同步事件
        applicationContext.publishEvent(new SyncFlowableProcessHisEvent(bpmnProcessInstance.getId()));
        return applyDetail(poId);
    }
    
    @Nullable
    @Override
    public DemoLeaveInfoDetailRespVO applyDetail(Long id) {
        Objects.requireNonNull(id, "id cannot be null.");
        DemoLeaveInfoPO po = getById(id);
        if (po == null) {
            return null;
        }
        DemoLeaveInfoDetailRespVO resp = new DemoLeaveInfoDetailRespVO();
        BeanUtils.copyProperties(po, resp);
        
        // ---------------------------------- 查询流程详情
        LeaveApprovalState approvalState = po.getApprovalState();
        Set<LeaveApprovalState> endSet = Sets.newHashSet(
                LeaveApprovalState.REVIEW_PASS,
                LeaveApprovalState.REVIEW_REJECT,
                LeaveApprovalState.AUDIT_REJECT
        );
        LocalDate poCreateAt = LocalDate.of(LocalDate.now().getYear(), 1, 1);
        
        String shardingDateFrom = DateTimeConverter.REGULAR_DATE.convert2Str(poCreateAt);
        String  shardingDateTo = DateTimeConverter.REGULAR_DATE.convert2Str(poCreateAt.plusYears(1).minusDays(1));
        boolean processHasEnd = endSet.contains(approvalState);
        // 获取对应的流程实例
        BpmnHisProcessInstance bpmnHisProcessInstance;
        if (processHasEnd) {
            bpmnHisProcessInstance = shardingBpmnHistoryService.detailHisProcessInstance(
                    ShardingProcessDetailQueryDTO.builder()
                            .businessKey(id + "")
                            .shardingDateFrom(shardingDateFrom)
                            .shardingDateTo(shardingDateTo)
                            .tenant(TENANT)
                            .build()
            );
        } else {
            bpmnHisProcessInstance = Optional.ofNullable(
                            historyService.createHistoricProcessInstanceQuery()
                                    .processInstanceTenantIdLike(TENANT + "%")
                                    .processInstanceBusinessKey(id + "")
                                    .singleResult()
                    ).map(BpmnEntityConvert::convert2BpmnHisProcessInstance)
                    .orElse(null);
        }
        if (bpmnHisProcessInstance != null) {
            List<Object> processDetailList = new ArrayList<>(); // 简单组装成string返回，需要的话你可以自行组装结构，相关数据详见processDetail方法
            processDetail(processHasEnd, shardingDateFrom, shardingDateTo,
                    processDetailList, bpmnHisProcessInstance.getId(), true);
            resp.setProcessDetailList(processDetailList);
        
            List<Object> processDetailListContainTodoNode = new ArrayList<>();// 简单组装成string返回，需要的话你可以自行组装结构，相关数据详见processDetailContainTodoNode方法
            processDetailContainTodoNode(processHasEnd, shardingDateFrom, shardingDateTo, processDetailListContainTodoNode,
                    bpmnHisProcessInstance.getProcessDefinitionId(), bpmnHisProcessInstance.getId(), true);
            resp.setProcessDetailListContainTodoNode(processDetailListContainTodoNode);
        }
        return resp;
    }
    
    @Override
    public Map<String, Object> taskDetail(String taskId) {
        LocalDate poCreateAt = LocalDate.of(LocalDate.now().getYear(), 1, 1);
        String shardingDateFrom = DateTimeConverter.REGULAR_DATE.convert2Str(poCreateAt); // 假设分片查询，查询的时间段为这个
        String  shardingDateTo = DateTimeConverter.REGULAR_DATE.convert2Str(poCreateAt.plusYears(1).minusDays(1));
        
        BpmnHisTask bpmnHisTask = shardingBpmnHistoryService.detailHisTask(
                ShardingTaskDetailQueryDTO.builder()
                        .taskId(taskId)
                        .shardingDateFrom(shardingDateFrom)
                        .shardingDateTo(shardingDateTo)
                        .tenant(TENANT)
                        .build()
        );
        // 再尝试从分片表中查询
        if (bpmnHisTask == null) {
            bpmnHisTask = shardingBpmnHistoryService.detailHisTask(
                    ShardingTaskDetailQueryDTO.builder()
                            .taskId(taskId)
                            .shardingDateFrom(shardingDateFrom)
                            .shardingDateTo(shardingDateTo)
                            .tenant(TENANT)
                            .build()
            );
        }
        
        Map<String, Object> detail = new HashMap<>(4);
        
        Objects.requireNonNull(bpmnHisTask);
        List<UserTask> bpmn20UserTasks = queryUserTaskFromBpmnModel(TENANT,
                bpmnHisTask.getProcessDefinitionId(), null);
        String taskDefinitionKey = bpmnHisTask.getTaskDefinitionKey();
        UserTask bpmn20UserTask = bpmn20UserTasks.stream().filter(x -> x.getId().equals(taskDefinitionKey)).findFirst().orElse(null);
        if (bpmn20UserTask != null) {
            detail.put("candidateUsers", bpmn20UserTask.getCandidateUsers());
            detail.put("candidateGroups", bpmn20UserTask.getCandidateGroups());
        }
        detail.put("bpmnHisTask", bpmnHisTask);
        return detail;
    }
    
    @Override
    public PageInfo<DemoLeaveInfoListRespVO> listTodoApply(DemoLeaveInfoListTodoTaskReqVO req) {
         Long userId = req.getUserId();
         LeaveApprovalState approvalState = req.getApprovalState();
         LeaveType type = req.getType();
         Integer pageNum = req.getPageNum();
         Integer pageSize = req.getPageSize();

        TaskQuery taskQuery = taskService.createTaskQuery();
        taskQuery.taskTenantIdLike(TENANT + "%");
        // ---------------------------- 按流程变量过滤 ----------------------------
        // 只查询这个流程实例分类下的task (注：因为我们发起请假流程时，就给这个流程设置了这个流程变量，用来给流程分类)
        taskQuery.processVariableValueEquals(FlowableProcessFlag.PROCESS_VAR_KEY, FlowableProcessFlag.LEAVE.name());
        // 查询指定approvalState状态流程下的task (注：因为发起流程时，我们也将这个变量放入流程变量中了，所以查询任务时也可以过滤这个参数)
        if (approvalState != null) {
            taskQuery.processVariableValueEquals("approvalState", approvalState.name());
        }
        // ---------------------------- 按任务处理人过滤 ----------------------------
        if (userId != null) {
            if (!userIdAndGroupMap.containsKey(userId)) {
                throw new RuntimeException("用户不存在，可用用户有：" + userIdAndGroupMap.keySet());
            }
            Collection<String> candidateGroups = userIdAndGroupMap.get(userId);
            taskQuery.or()
                    .taskAssignee(userId + "")
                    .taskCandidateUser(userId + "");
            if(!CollectionUtils.isEmpty(candidateGroups)) {
                taskQuery.taskCandidateGroupIn(candidateGroups);
            }
            taskQuery.endOr();
        }
    
        // ---------------------------- 查询出工作流中的任务（，并找出关联的业务数据的id） ----------------------------
        List<? extends BpmnTask> taskList = Optional.ofNullable(
                taskQuery.list()
                ).map(x -> x.stream()
                        .map(BpmnEntityConvert::convert2BpmnTask)
                        .toList()
                ).orElse(Collections.emptyList());
        Set<Long> bizIdSet = null;
        Set<String> processIdSet = taskList.stream().map(BpmnTask::getProcessInstanceId).collect(Collectors.toSet());
        if (!CollectionUtils.isEmpty(processIdSet)) {
            List<? extends BpmnProcessInstance> processInstancesList = Optional.ofNullable(
                            runtimeService.createProcessInstanceQuery().processInstanceTenantIdLike(TENANT + "%")
                                    .processInstanceIds(processIdSet).list()
                    ).map(x -> x.stream().map(BpmnEntityConvert::convert2BpmnProcessInstance).collect(Collectors.toList()))
                    .orElse(Collections.emptyList());

            bizIdSet = processInstancesList.stream().map(x -> Long.parseLong(x.getBusinessKey()))
                    .collect(Collectors.toSet());
        }
        if (CollectionUtils.isEmpty(bizIdSet)) {
            return PageInfo.of(0, pageNum, pageSize);
        }
    
        // ---------------------------- 根据业务数据id，关联查询业务数据 ----------------------------
        IPage<DemoLeaveInfoPO> pageInfo = new Page<>(pageNum, pageSize);
        IPage<DemoLeaveInfoPO> page = this.baseMapper.selectPage(pageInfo, new LambdaQueryWrapper<DemoLeaveInfoPO>()
                // 业务字段type没有放进工作流变量中的话，需要在查询业务数据时再过滤
                .eq(type != null,  DemoLeaveInfoPO::getType, type)
                .eq(approvalState != null,  DemoLeaveInfoPO::getApprovalState, approvalState)
                .in(DemoLeaveInfoPO::getId, bizIdSet)
                .orderByDesc(DemoLeaveInfoPO::getId)
        );
        List<DemoLeaveInfoPO> records = page.getRecords();
        if (records.size() != bizIdSet.size()) {
            log.warn("数据库 和 工作流流程变量中的approvalState数据不一致(理论上这不应该发生). ids -> {}",
                    bizIdSet.removeAll(records.stream().map(DemoLeaveInfoPO::getId).collect(Collectors.toSet())));
        }
        List<DemoLeaveInfoListRespVO> list;
        if (CollectionUtils.isEmpty(records)) {
            list = Collections.emptyList();
        } else {
            list = records.stream().map(po -> {
                DemoLeaveInfoListRespVO resp = new DemoLeaveInfoListRespVO();
                BeanUtils.copyProperties(po, resp);
                return resp;
            }).collect(Collectors.toList());
        }
        return PageInfo.of(page.getTotal(), pageNum, pageSize, list);
    }
    
    @Override
    public PageInfo<DemoLeaveInfoListRespVO> listDoneApply(DemoLeaveInfoListDoneTaskReqVO req) {
        Long userId = req.getUserId();
        LeaveApprovalState approvalState = req.getApprovalState();
        LeaveType type = req.getType();
        Integer pageNum = req.getPageNum();
        Integer pageSize = req.getPageSize();
    
        LocalDate poCreateAt = LocalDate.of(LocalDate.now().getYear(), 1, 1);
        String shardingDateFrom = DateTimeConverter.REGULAR_DATE.convert2Str(poCreateAt); // 假设分片查询，查询的时间段为这个
        String  shardingDateTo = DateTimeConverter.REGULAR_DATE.convert2Str(poCreateAt.plusYears(1).minusDays(1));
    
        ShardingTaskListQueryDTO shardingTaskListQuery = ShardingTaskListQueryDTO.builder()
                .shardingDateFrom(shardingDateFrom)
                .shardingDateTo(shardingDateTo)
                .tenant(TENANT)
                .ifFinished(true)
                .queryTaskLocalVariables(true)
                .orderByTaskEndTime(true).
                build();
    
        // ---------------------------- 按流程变量过滤 ----------------------------
        Collection<StrObjVar> processVarColl = new ArrayList<>();
        // 只查询这个流程实例分类下的task (注：因为我们发起请假流程时，就给这个流程设置了这个流程变量，用来给流程分类)
        processVarColl.add(StrObjVar.of(FlowableProcessFlag.PROCESS_VAR_KEY, FlowableProcessFlag.LEAVE.name()));
        shardingTaskListQuery.setProcessVariableValueEqualsColl(processVarColl);
    

        // ---------------------------- 按任务处理人过滤 ----------------------------
        if (userId != null) {
            if (!userIdAndGroupMap.containsKey(userId)) {
                throw new RuntimeException("用户不存在，可用用户有：" + userIdAndGroupMap.keySet());
            }
            // 完成任务时， 使用的是【将任务完成人记录到任务本地变量】的方式，来记录任务完成人，那么这里查询时就用任务的本地变量来过滤
            shardingTaskListQuery.setTaskVariableValueEqualsColl(
                    Lists.newArrayList(
                            StrObjVar.of(LOCAL_TASK_COMPLETED_BY, userId)
                    )
            );
        }
    
        // ---------------------------- 查询出工作流中的任务（，并找出关联的业务数据的id） ----------------------------
        Set<Long> bizIdSet = null;
        
        List<? extends BpmnHisTask> taskList = shardingBpmnHistoryService.listHisTask(shardingTaskListQuery);
        Set<String> processIdSet = taskList.stream().map(BpmnTask::getProcessInstanceId).collect(Collectors.toSet());
        if (!CollectionUtils.isEmpty(processIdSet)) {
            List<? extends BpmnHisProcessInstance> bpmnHisProcessInstances =
                    shardingBpmnHistoryService.listHisProcessInstance(
                            ShardingProcessListQueryDTO.builder()
                                    .shardingDateFrom(shardingDateFrom)
                                    .shardingDateTo(shardingDateTo)
                                    .tenant(TENANT)
                                    .processInstanceIds(processIdSet)
                                    .build());
            bizIdSet = bpmnHisProcessInstances.stream().map(x -> Long.parseLong(x.getBusinessKey()))
                    .collect(Collectors.toSet());
        }
        if (CollectionUtils.isEmpty(bizIdSet)) {
            return PageInfo.of(0, pageNum, pageSize);
        }
    
        // ---------------------------- 根据业务数据id，关联查询业务数据 ----------------------------
        IPage<DemoLeaveInfoPO> pageInfo = new Page<>(pageNum, pageSize);
        IPage<DemoLeaveInfoPO> page = this.baseMapper.selectPage(pageInfo, new LambdaQueryWrapper<DemoLeaveInfoPO>()
                // 业务字段type没有放进工作流变量中的话，需要在查询业务数据时再过滤
                .eq(type != null,  DemoLeaveInfoPO::getType, type)
                .eq(approvalState != null,  DemoLeaveInfoPO::getApprovalState, approvalState)
                .in(DemoLeaveInfoPO::getId, bizIdSet)
                .orderByDesc(DemoLeaveInfoPO::getId)
        );
        List<DemoLeaveInfoPO> records = page.getRecords();
        if (records.size() != bizIdSet.size()) {
            log.warn("数据库 和 工作流流程变量中的approvalState数据不一致(理论上这不应该发生). ids -> {}",
                    bizIdSet.removeAll(records.stream().map(DemoLeaveInfoPO::getId).collect(Collectors.toSet())));
        }
        List<DemoLeaveInfoListRespVO> list;
        if (CollectionUtils.isEmpty(records)) {
            list = Collections.emptyList();
        } else {
            list = records.stream().map(po -> {
                DemoLeaveInfoListRespVO resp = new DemoLeaveInfoListRespVO();
                BeanUtils.copyProperties(po, resp);
                return resp;
            }).collect(Collectors.toList());
        }
        return PageInfo.of(page.getTotal(), pageNum, pageSize, list);
    }
    
    @Override
    public PageInfo<DemoLeaveInfoListRespVO> listLaunchApply(DemoLeaveInfoListLaunchTaskReqVO req) {
        LocalDate poCreateAt = LocalDate.of(LocalDate.now().getYear(), 1, 1);
        String shardingDateFrom = DateTimeConverter.REGULAR_DATE.convert2Str(poCreateAt); // 假设分片查询，查询的时间段为这个
        String  shardingDateTo = DateTimeConverter.REGULAR_DATE.convert2Str(poCreateAt.plusYears(1).minusDays(1));
        
        Long userId = req.getUserId();
        LeaveApprovalState approvalState = req.getApprovalState();
        LeaveType type = req.getType();
        Integer pageNum = req.getPageNum();
        Integer pageSize = req.getPageSize();
    
        // ---------------------------- 查询流程中的数据 ----------------------------
        Collection<StrObjVar> processVarColl = new ArrayList<>();
        // 只查询这个流程实例分类下的task (注：因为我们发起请假流程时，就给这个流程设置了这个流程变量，用来给流程分类)
        processVarColl.add(StrObjVar.of(FlowableProcessFlag.PROCESS_VAR_KEY, FlowableProcessFlag.LEAVE.name()));
        // 查询指定approvalState状态流程下的task (注：因为发起流程时，我们也将这个变量放入流程变量中了，所以查询任务时也可以过滤这个参数)
        if (approvalState != null) {
            processVarColl.add(StrObjVar.of("approvalState", approvalState.name()));
        }
    
        ShardingProcessListQueryDTO shardingProcessListQuery = ShardingProcessListQueryDTO.builder()
                .shardingDateFrom(shardingDateFrom)
                .shardingDateTo(shardingDateTo)
                .tenant(TENANT)
                .processVariableValueEqualsColl(processVarColl)
                .build();
        
        if (userId != null) {
            if (!userIdAndGroupMap.containsKey(userId)) {
                throw new RuntimeException("用户不存在，可用用户有：" + userIdAndGroupMap.keySet());
            }
            shardingProcessListQuery.setStartedBy(userId + "");
        }
        shardingProcessListQuery.setQueryProcessVariables(true); // 是否查询流程变量
    
        // ---------------------------- 查询出工作流中的流程（，并找出关联的业务数据的id） ----------------------------
        List<? extends BpmnHisProcessInstance> processList = shardingBpmnHistoryService.listHisProcessInstance(shardingProcessListQuery);
        Set<Long> bizIdSet = processList.stream().map(x -> Long.parseLong(x.getBusinessKey()))
                    .collect(Collectors.toSet());
        if (CollectionUtils.isEmpty(bizIdSet)) {
            return PageInfo.of(0, pageNum, pageSize);
        }
        
        // ---------------------------- 根据业务数据id，关联查询业务数据 ----------------------------
        IPage<DemoLeaveInfoPO> pageInfo = new Page<>(pageNum, pageSize);
        IPage<DemoLeaveInfoPO> page = this.baseMapper.selectPage(pageInfo, new LambdaQueryWrapper<DemoLeaveInfoPO>()
                // 业务字段type没有放进工作流变量中的话，需要在查询业务数据时再过滤
                .eq(type != null,  DemoLeaveInfoPO::getType, type)
                .eq(approvalState != null,  DemoLeaveInfoPO::getApprovalState, approvalState)
                .in(DemoLeaveInfoPO::getId, bizIdSet)
                .orderByDesc(DemoLeaveInfoPO::getId)
        );
        List<DemoLeaveInfoPO> records = page.getRecords();
        if (records.size() != bizIdSet.size()) {
            log.warn("数据库 和 工作流流程变量中的approvalState数据不一致(理论上这不应该发生). ids -> {}",
                    bizIdSet.removeAll(records.stream().map(DemoLeaveInfoPO::getId).collect(Collectors.toSet())));
        }
        List<DemoLeaveInfoListRespVO> list;
        if (CollectionUtils.isEmpty(records)) {
            list = Collections.emptyList();
        } else {
            list = records.stream().map(po -> {
                DemoLeaveInfoListRespVO resp = new DemoLeaveInfoListRespVO();
                BeanUtils.copyProperties(po, resp);
                return resp;
            }).collect(Collectors.toList());
        }
        return PageInfo.of(page.getTotal(), pageNum, pageSize, list);
    }
    
    @Override
    public PageInfo<DemoLeaveInfoListRespVO> list(DemoLeaveInfoListReqVO req) {
        int pageNum = req.getPageNum();
        int pageSize = req.getPageSize();
        String summary = req.getSummary();
        LeaveType type = req.getType();
        LocalDateTime leaveFrom = req.getLeaveFrom();
        LocalDateTime leaveTo = req.getLeaveTo();
        LeaveApprovalState approvalState = req.getApprovalState();

        // 分页查
        IPage<DemoLeaveInfoPO> pageInfo = new Page<>(pageNum, pageSize);
        IPage<DemoLeaveInfoPO> page = this.baseMapper.selectPage(pageInfo, new LambdaQueryWrapper<DemoLeaveInfoPO>()
            .eq(summary != null && !summary.trim().isEmpty(),  DemoLeaveInfoPO::getSummary, summary)
            .eq(type != null,  DemoLeaveInfoPO::getType, type)
            .eq(leaveFrom != null,  DemoLeaveInfoPO::getLeaveFrom, leaveFrom)
            .eq(leaveTo != null,  DemoLeaveInfoPO::getLeaveTo, leaveTo)
            .eq(approvalState != null,  DemoLeaveInfoPO::getApprovalState, approvalState)
            .orderByDesc(DemoLeaveInfoPO::getId)
        );
        // 转换为resp模型
        List<DemoLeaveInfoPO> records = page.getRecords();
        List<DemoLeaveInfoListRespVO> list;
        if (CollectionUtils.isEmpty(records)) {
            list = Collections.emptyList();
        } else {
            list = records.stream().map(po -> {
                DemoLeaveInfoListRespVO resp = new DemoLeaveInfoListRespVO();
                BeanUtils.copyProperties(po, resp);
                return resp;
            }).collect(Collectors.toList());
        }
        return PageInfo.of(page.getTotal(), pageNum, pageSize, list);
    }
    
    /**
     * 发起流程
     */
    @SneakyThrows
    private void releaseProcess(DemoLeaveInfoPO bizPo) {
        // --------------------- 发起审批流程 ---------------------
        String businessKey = bizPo.getId() + ""; // 可考虑使用对应的业务数据id （注：方便后续关联查询）
        Map<String, Object> variables = new HashMap<>(Map.of(
                "sponsor", userId, // 流程中设置的动态发起人
                "approver", approvalId, // 流程中设置的动态初审人
                "approvalState", bizPo.getApprovalState().name(), // 流程中要用到的变量
                FlowableProcessFlag.PROCESS_VAR_KEY, FlowableProcessFlag.LEAVE.name() // 给这个流程一个分类标识，便于后续查询
        ));
    
    
        
    
    
        // 检查流程变量是否满足(启动节点设置的表单约束)
        BpmnFormData startFormData = bpmnFormService.getStartFormData(TENANT, processDefinitionId);
        List<? extends BpmnFormProperty> bpmnFormProperties =
                Optional.ofNullable(startFormData).map(BpmnFormData::getFormProperties).orElse(Collections.emptyList());
        JSONObject bizPoMap = (JSONObject) JSON.toJSON(bizPo);
        for (BpmnFormProperty bpmnFormProperty : bpmnFormProperties) {
            String key = bpmnFormProperty.getKey();
            Boolean ifRequired = bpmnFormProperty.getIfRequired();
            String valueType = bpmnFormProperty.getType();
            String defaultValue = bpmnFormProperty.getValue();
            Object value = variables.get(key);
            if (value != null) {
                continue;
            }
            Class<?> clazz = StringUtils.isBlank(valueType) ? String.class: Class.forName(valueType);
            value = bizPoMap.getObject(key, clazz);
            if (value == null && defaultValue != null) {
                value = defaultValue;
            }
            if (value == null) {
                if (ifRequired != null && ifRequired) {
                    throw new IllegalStateException("启动流程实例时，缺少流程变量" + key);
                }
            } else {
                variables.put(key, value);
            }
        }
        
        // 启动流程实例
        ProcessInstance processInstance;
        try {
            // Flowable发起流程时，会直接从登录态中获取当前用户作为流程发起人
            Authentication.setAuthenticatedUserId(userId + "");
            processInstance = runtimeService.startProcessInstanceByKeyAndTenantId(processDefinitionKey, businessKey,
                    variables, TENANT);
        } finally {
            Authentication.setAuthenticatedUserId(null);
        }
        BpmnProcessInstance bpmnProcessInstance = BpmnEntityConvert.convert2BpmnProcessInstance(processInstance);
    
        BpmnTask bpmnTask = Optional.ofNullable(
                        taskService.createTaskQuery()
                                .taskTenantIdLike(TENANT + "%")
                                .processInstanceId(bpmnProcessInstance.getId())
                                .singleResult()
                ).map(BpmnEntityConvert::convert2BpmnTask)
                .orElse(null);
        Objects.requireNonNull(bpmnTask, "task should not be null.");
        // 根据流程设计文件，第一个节点【发起申请】，那这里完成一下自己的节点，使流程可以继续往下流转
        taskService.setVariableLocal(bpmnTask.getId(), LOCAL_TASK_COMPLETED_BY, userId);
        taskService.complete(bpmnTask.getId(), userId + "", null);
    
        // 发布历史数据同步事件
        applicationContext.publishEvent(new SyncFlowableProcessHisEvent(bpmnProcessInstance.getId()));
    }
    
    /**
     * 流程进度详情-含未知节点
     */
    public void processDetailContainTodoNode(boolean processHasEnd, String shardingDateFrom, String shardingDateTo, 
                                             List<Object> infoList, String processDefinitionId, String processInstanceId,
                                             boolean structRecord) {
        Triple<List<? extends BpmnHisTask>, Map<String, ? extends List<? extends BpmnComment>>, Map<String, ?
                extends List<? extends BpmnAttachment>>> triple = processInfo(processHasEnd,
                shardingDateFrom, shardingDateTo, processInstanceId);
        List<? extends BpmnHisTask> bpmnHisTaskList = triple.getLeft();
        Map<String, ? extends List<? extends BpmnComment>> taskIdAndCommentsMap = triple.getMiddle();
        Map<String, ? extends List<? extends BpmnAttachment>> taskIdAndAttachmentsMap = triple.getRight();
    
        // -------------------------- 已知的节点 --------------------------
        for (int i = 0; i < bpmnHisTaskList.size(); i++) {
            BpmnHisTask bpmnHisTask = bpmnHisTaskList.get(i);
            String taskId = bpmnHisTask.getId();
            if (structRecord) {
                infoList.add(
                        MapUtil.ofEntries(
                                MapUtil.entry("已知节点", i + 1),
                                MapUtil.entry("任务信息", bpmnHisTask),
                                MapUtil.entry("评论信息", taskIdAndCommentsMap.get(taskId)),
                                MapUtil.entry("附件信息", taskIdAndAttachmentsMap.get(taskId)),
                                MapUtil.entry("任务状态", (bpmnHisTask.getEndTime() == null ? "待处理" : "已完成"))
                        )
                );
            } else {
                infoList.add(String.format("【已知节点-%s】：", (i + 1)) +
                        "任务信息：" + formatStr(bpmnHisTask)
                        + "\n 评论信息：" + formatStr(taskIdAndCommentsMap.get(taskId))
                        + "\n 附件信息：" + formatStr(taskIdAndAttachmentsMap.get(taskId))
                        + "\n 任务状态：" + (bpmnHisTask.getEndTime() == null ? "待处理" : "已完成")
                );
            }
        }
        
        Set<String> knownTaskDefKeySet = bpmnHisTaskList.stream().map(BpmnTask::getTaskDefinitionKey).collect(Collectors.toSet());
        
        
        // -------------------------- 未知的节点（已执行或执行中的任务为已知节点，后续的为未知节点） --------------------------
        List<UserTask> bpmn20UserTasks = queryUserTaskFromBpmnModel(TENANT,
                processDefinitionId, null);
        
        int count = 1;
        for (UserTask bpmn20UserTask : bpmn20UserTasks) {
            boolean contains = knownTaskDefKeySet.contains(bpmn20UserTask.getId());
            if (contains) {
                continue;
            }
            String assignee = bpmn20UserTask.getAssignee();
            List<String> candidateUsers = bpmn20UserTask.getCandidateUsers();
            List<String> candidateGroups = bpmn20UserTask.getCandidateGroups();
            String description = bpmn20UserTask.getDocumentation();
            String name = bpmn20UserTask.getName();
            if (structRecord) {
                infoList.add(
                        MapUtil.ofEntries(
                                MapUtil.entry("未知节点", count + ""),
                                MapUtil.entry("任务名", name),
                                MapUtil.entry("任务描述", StringUtils.defaultIfBlank(description, "（无）")),
                                MapUtil.entry("办理人", StringUtils.defaultIfBlank(assignee, "（未指定）")),
                                MapUtil.entry("候选人", CollectionUtils.isEmpty(candidateUsers) ? "（无）" : String.join("、", candidateUsers)),
                                MapUtil.entry("候选组", CollectionUtils.isEmpty(candidateGroups) ? "（无）" : String.join("、", candidateGroups))
                        )
                );
            } else {
                infoList.add(
                        String.format("未知节点-%s：\n\t\t任务名：%s\n\t\t任务描述:%s\n\t\t办理人：%s\n\t\t候选人：%s\n\t\t候选组：%s",
                                count,
                                name,
                                StringUtils.defaultIfBlank(description, "（无）"),
                                StringUtils.defaultIfBlank(assignee, "（未指定）"),
                                CollectionUtils.isEmpty(candidateUsers) ? "（无）" : String.join("、", candidateUsers),
                                CollectionUtils.isEmpty(candidateGroups) ? "（无）" : String.join("、", candidateGroups)
                        )
                );
            }
            
            count++;
        }
    }
    
    /**
     * 流程进度详情-不含未知节点
     */
    public void processDetail(boolean processHasEnd, String shardingDateFrom, String shardingDateTo,
                              List<Object> infoList, String processInstanceId, boolean structRecord) {
        Triple<List<? extends BpmnHisTask>, Map<String, ? extends List<? extends BpmnComment>>, Map<String, ?
                extends List<? extends BpmnAttachment>>> triple = processInfo(processHasEnd,
                shardingDateFrom, shardingDateTo, processInstanceId);
        List<? extends BpmnHisTask> bpmnHisTaskList = triple.getLeft();
        Map<String, ? extends List<? extends BpmnComment>> taskIdAndCommentsMap = triple.getMiddle();
        Map<String, ? extends List<? extends BpmnAttachment>> taskIdAndAttachmentsMap = triple.getRight();
        
        for (int i = 0; i < bpmnHisTaskList.size(); i++) {
            BpmnHisTask bpmnHisTask = bpmnHisTaskList.get(i);
            String taskId = bpmnHisTask.getId();
            if (structRecord) {
                infoList.add(
                        MapUtil.ofEntries(
                                MapUtil.entry("节点", i + 1),
                                MapUtil.entry("任务信息", bpmnHisTask),
                                MapUtil.entry("评论信息", taskIdAndCommentsMap.get(taskId)),
                                MapUtil.entry("附件信息", taskIdAndAttachmentsMap.get(taskId)),
                                MapUtil.entry("任务状态", (bpmnHisTask.getEndTime() == null ? "待处理" : "已完成"))
                        )
                );
            } else {
                infoList.add(String.format("【节点%s】：", (i + 1)) +
                        "任务信息：" + formatStr(bpmnHisTask)
                        + "\n 评论信息：" + formatStr(taskIdAndCommentsMap.get(taskId))
                        + "\n 附件信息：" + formatStr(taskIdAndAttachmentsMap.get(taskId))
                        + "\n 任务状态：" + (bpmnHisTask.getEndTime() == null ? "待处理" : "已完成")
                );
            }
        }
    }
    private static String formatStr(Object obj) {
        return com.alibaba.fastjson2.JSON.toJSONString(obj, JSONWriter.Feature.PrettyFormat);
    }
    
    /**
     * 查询流程相关信息
     *
     * @param processHasEnd 流程是否已结束
     * @param shardingDateFrom 若processHasEnd=true,则从历史分片表中插叙你数据，此参数为查询时间-起
     * @param shardingDateTo 若processHasEnd=true,则从历史分片表中插叙你数据，此参数为查询时间-止
     * @param processInstanceId 流程实例id
     *
     * @return  左-任务列表；中-评论（key为任务id）；右-附件（key为任务id）
     */
    private Triple<
            List<? extends BpmnHisTask>,
            Map<String, ? extends List<? extends BpmnComment>>,
            Map<String, ? extends List<? extends BpmnAttachment>>
            > processInfo(boolean processHasEnd, String shardingDateFrom, String shardingDateTo, String processInstanceId) {
        // 2. 查询流程各任务节点
        List<? extends BpmnHisTask> bpmnHisTaskList;
        if (processHasEnd) {
            bpmnHisTaskList = shardingBpmnHistoryService.listHisTask(
                    ShardingTaskListQueryDTO.builder()
                            .shardingDateFrom(shardingDateFrom)
                            .shardingDateTo(shardingDateTo)
                            .tenant(TENANT)
                            .processInstanceId(processInstanceId)
                            .orderByTaskStartTime(true)
                            .build()
            );
        } else {
            bpmnHisTaskList = Optional.ofNullable(
                    historyService.createHistoricTaskInstanceQuery()
                            .taskTenantIdLike(TENANT + "%")
                            .processInstanceId(processInstanceId)
                            .orderByHistoricTaskInstanceStartTime().asc()
                            .list()
            ).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnHisTask)
                    .toList()
            ).orElse(Collections.emptyList());
        }
        
        // 流程评论
        List<? extends BpmnComment> processInstanceCommentList;
        if (processHasEnd) {
            processInstanceCommentList = shardingBpmnHistoryService.getProcessInstanceComments(
                    ShardingCommentListQueryDTO.builder()
                            .shardingDateFrom(shardingDateFrom)
                            .shardingDateTo(shardingDateTo)
                            .processInstanceId(processInstanceId)
                            .build()
            );
        } else {
            processInstanceCommentList = Optional.ofNullable(
                            taskService.getProcessInstanceComments(processInstanceId)
                    ).map(x -> x.stream().map(BpmnEntityConvert::convert2BpmnComment).collect(Collectors.toList()))
                    .orElse(Collections.emptyList());
        }
        // 任务评论
        Map<String, ? extends List<? extends BpmnComment>> taskIdAndCommentsMap =
                processInstanceCommentList.stream().filter(x -> Objects.nonNull(x.getTaskId()) && "comment".equals(x.getType()))
                        .collect(Collectors.groupingBy(BpmnComment::getTaskId));
        // 流程附件
        List<? extends BpmnAttachment> processInstanceAttachments;
        if (processHasEnd) {
            processInstanceAttachments = shardingBpmnHistoryService.getProcessInstanceAttachments(
                    ShardingAttachmentListQueryDTO.builder()
                            .shardingDateFrom(shardingDateFrom)
                            .shardingDateTo(shardingDateTo)
                            .processInstanceId(processInstanceId)
                            .build()
            );
        } else {
            processInstanceAttachments = Optional.ofNullable(
                            taskService.getProcessInstanceAttachments(processInstanceId)
                    ).map(x -> x.stream().map(BpmnEntityConvert::convert2BpmnAttachment).collect(Collectors.toList()))
                    .orElse(Collections.emptyList());
        }
        // 任务附件
        Map<String, ? extends List<? extends BpmnAttachment>> taskIdAndAttachmentsMap =
                processInstanceAttachments.stream().filter(x -> Objects.nonNull(x.getTaskId()))
                        .collect(Collectors.groupingBy(BpmnAttachment::getTaskId));
        return Triple.of(bpmnHisTaskList, taskIdAndCommentsMap, taskIdAndAttachmentsMap);
    }

    /**
     * 从bpmn模型中查询流程定义的用户任务
     *
     * @param tenant 租户
     * @param processDefId 流程定义id
     * @param xmlProcessId bpmn模型中的流程id <br />注：bpmn xml文件中，Process元素的id属性值
     *
     * @return 流程定义的用户任务
     */
    @Nonnull
    public List<UserTask> queryUserTaskFromBpmnModel(@Nonnull String tenant, @Nonnull String processDefId,
                                                     @Nullable String xmlProcessId) {
        // 获取流程定义的BpmnModel，其中包含了流程的所有节点信息
        BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefId);
        if (bpmnModel == null) {
            throw new RuntimeException("Not found BpmnModel.");
        }
        // 获取流程定义的主流程
        Process process;
        if (StringUtils.isNotBlank(xmlProcessId)) {
            process = bpmnModel.getProcessById(xmlProcessId);
        } else {
            process = bpmnModel.getMainProcess();
        }
        if (process == null) {
            throw new RuntimeException("Not found Process.");
        }

        List<UserTask> list = new ArrayList<>();
        // 此方法只关注用户任务，其余方法忽略
        for (FlowElement flowElement : process.getFlowElements()) {
            if (flowElement instanceof UserTask userTask) {
                list.add(userTask);
            }
        }
        return list;
    }
}